//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2021 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import _Concurrency
import Basics
import Dispatch
import Foundation
import PackageModel
import SourceControl

import class TSCBasic.BufferedOutputByteStream
import struct TSCBasic.ByteString
import class Basics.AsyncProcess
import struct Basics.AsyncProcessResult

import enum TSCUtility.Diagnostics
import struct TSCUtility.Version

#if os(Windows)
import WinSDK
#endif

extension AbsolutePath {
    /// Returns the `pathString` on non-Windows platforms.  On Windows
    /// platforms, this provides the path string normalized as per the Windows
    /// path normalization rules.  In the case that the path is a long path, the
    /// path will use the extended path syntax (UNC style, NT Path).
    internal var _normalized: String {
#if os(Windows)
        return self.pathString.withCString(encodedAs: UTF16.self) { pwszPath in
            let dwLength = GetFullPathNameW(pwszPath, 0, nil, nil)
            return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) {
                _ = GetFullPathNameW(pwszPath, dwLength, $0.baseAddress, nil)
                return String(decodingCString: $0.baseAddress!, as: UTF16.self)
            }
        }
#else
        return self.pathString
#endif
    }
}

public enum ManifestParseError: Swift.Error, Equatable {
    /// The manifest is empty, or at least from SwiftPM's perspective it is.
    case emptyManifest(path: AbsolutePath)
    /// The manifest contains invalid format.
    case invalidManifestFormat(String, diagnosticFile: AbsolutePath?)
    // TODO: Test this error.
    case invalidManifestFormat(String, diagnosticFile: AbsolutePath?, compilerCommandLine: [String]?)

    /// The manifest was successfully loaded by swift interpreter but there were runtime issues.
    case runtimeManifestErrors([String])

    /// The manifest loader specified import restrictions that the given manifest violated.
    case importsRestrictedModules([String])

    /// The JSON payload received from executing the manifest has an unsupported version, usually indicating an invalid mix-and-match of SwiftPM and PackageDescription libraries.
    case unsupportedVersion(version: Int, underlyingError: String? = nil)
}

// used to output the errors via the observability system
extension ManifestParseError: CustomStringConvertible {
    public var description: String {
        switch self {
        case .emptyManifest(let manifestPath):
            return "'\(manifestPath)' is empty"
        case .invalidManifestFormat(let error, _, let compilerCommandLine):
            let suffix: String
            if let compilerCommandLine {
                suffix = " (compiled with: \(compilerCommandLine))"
            } else {
                suffix = ""
            }
            return "Invalid manifest\(suffix)\n\(error)"
        case .runtimeManifestErrors(let errors):
            return "invalid manifest (evaluation failed)\n\(errors.joined(separator: "\n"))"
        case .importsRestrictedModules(let modules):
            return "invalid manifest, imports restricted modules: \(modules.joined(separator: ", "))"
        case .unsupportedVersion(let version, let underlyingError):
            let message = "serialized JSON uses unsupported version \(version), indicating use of a mismatched PackageDescription library"
            if let underlyingError {
                return "\(message), underlying error: \(underlyingError)"
            }
            return message
        }
    }
}

// MARK: - ManifestLoaderProtocol

/// Protocol for the manifest loader interface.
public protocol ManifestLoaderProtocol {
    /// Load the manifest for the package at `path`.
    ///
    /// - Parameters:
    ///   - manifestPath: The root path of the package.
    ///   - manifestToolsVersion: The version of the tools the manifest supports.
    ///   - packageIdentity: the identity of the package
    ///   - packageKind: The kind of package the manifest is from.
    ///   - packageLocation: The location the package the manifest was loaded from.
    ///   - packageVersion: Optional. The version and revision of the package.
    ///   - identityResolver: A helper to resolve identities based on configuration
    ///   - dependencyMapper: A helper to map dependencies.
    ///   - fileSystem: File system to load from.
    ///   - observabilityScope: Observability scope to emit diagnostics.
    ///   - callbackQueue: The dispatch queue to perform completion handler on.
    ///   - completion: The completion handler .
    func load(
        manifestPath: AbsolutePath,
        manifestToolsVersion: ToolsVersion,
        packageIdentity: PackageIdentity,
        packageKind: PackageReference.Kind,
        packageLocation: String,
        packageVersion: (version: Version?, revision: String?)?,
        identityResolver: IdentityResolver,
        dependencyMapper: DependencyMapper,
        fileSystem: FileSystem,
        observabilityScope: ObservabilityScope,
        delegateQueue: DispatchQueue
    ) async throws -> Manifest

    /// Reset any internal cache held by the manifest loader.
    func resetCache(observabilityScope: ObservabilityScope) async

    /// Reset any internal cache held by the manifest loader and purge any entries in a shared cache
    func purgeCache(observabilityScope: ObservabilityScope) async
}

public protocol ManifestLoaderDelegate: Sendable {
    func willLoad(
        packageIdentity: PackageIdentity,
        packageLocation: String,
        manifestPath: AbsolutePath
    )
    func didLoad(
        packageIdentity: PackageIdentity,
        packageLocation: String,
        manifestPath: AbsolutePath,
        duration: DispatchTimeInterval
    )

    func willParse(
        packageIdentity: PackageIdentity,
        packageLocation: String
    )
    func didParse(
        packageIdentity: PackageIdentity,
        packageLocation: String,
        duration: DispatchTimeInterval
    )

    func willCompile(
        packageIdentity: PackageIdentity,
        packageLocation: String,
        manifestPath: AbsolutePath
    )
    func didCompile(
        packageIdentity: PackageIdentity,
        packageLocation: String,
        manifestPath: AbsolutePath,
        duration: DispatchTimeInterval
    )

    func willEvaluate(
        packageIdentity: PackageIdentity,
        packageLocation: String,
        manifestPath: AbsolutePath
    )
    func didEvaluate(
        packageIdentity: PackageIdentity,
        packageLocation: String,
        manifestPath: AbsolutePath,
        duration: DispatchTimeInterval
    )
}

// loads a manifest given a package root path
// this will first find the most appropriate manifest file in the package directory
// bases on the toolchain's tools-version and proceed to load that manifest
extension ManifestLoaderProtocol {
    public func load(
        packagePath: AbsolutePath,
        packageIdentity: PackageIdentity,
        packageKind: PackageReference.Kind,
        packageLocation: String,
        packageVersion: (version: Version?, revision: String?)?,
        currentToolsVersion: ToolsVersion,
        identityResolver: IdentityResolver,
        dependencyMapper: DependencyMapper,
        fileSystem: FileSystem,
        observabilityScope: ObservabilityScope,
        delegateQueue: DispatchQueue
    ) async throws -> Manifest {
        // find the manifest path and parse it's tools-version
        let manifestPath = try ManifestLoader.findManifest(
            packagePath: packagePath,
            fileSystem: fileSystem,
            currentToolsVersion: currentToolsVersion
        )
        let manifestToolsVersion = try ToolsVersionParser.parse(manifestPath: manifestPath, fileSystem: fileSystem)
        // validate the manifest tools-version against the toolchain tools-version
        try manifestToolsVersion.validateToolsVersion(
            currentToolsVersion,
            packageIdentity: packageIdentity,
            packageVersion: packageVersion?.version?.description ?? packageVersion?.revision
        )

        return try await self.load(
            manifestPath: manifestPath,
            manifestToolsVersion: manifestToolsVersion,
            packageIdentity: packageIdentity,
            packageKind: packageKind,
            packageLocation: packageLocation,
            packageVersion: packageVersion,
            identityResolver: identityResolver,
            dependencyMapper: dependencyMapper,
            fileSystem: fileSystem,
            observabilityScope: observabilityScope,
            delegateQueue: delegateQueue
        )
    }
}

// MARK: - ManifestCacheActor

actor ManifestCacheActor {
    private var memoryCache: [ManifestLoader.CacheKey: ManifestJSONParser.Result] = [:]

    func get(key: ManifestLoader.CacheKey) -> ManifestJSONParser.Result? {
        memoryCache[key]
    }

    func set(key: ManifestLoader.CacheKey, value: ManifestJSONParser.Result) {
        memoryCache[key] = value
    }

    func clear() {
        memoryCache.removeAll()
    }
}

// MARK: - ManifestLoader

/// Utility class for loading manifest files.
///
/// This class is responsible for reading the manifest data and produce a
/// properly formed `PackageModel.Manifest` object. It currently does so by
/// interpreting the manifest source using Swift -- that produces a JSON
/// serialized form of the manifest (as implemented by `PackageDescription`'s
/// `atexit()` handler) which is then deserialized and loaded.
public final class ManifestLoader: ManifestLoaderProtocol {
    public typealias Delegate = ManifestLoaderDelegate

    private let toolchain: UserToolchain
    private let serializedDiagnostics: Bool
    private let isManifestSandboxEnabled: Bool
    private let extraManifestFlags: [String]
    private let importRestrictions: (startingToolsVersion: ToolsVersion, allowedImports: [String])?
    private let pruneDependencies: Bool

    // not thread safe
    public var delegate: Delegate?

    private let databaseCacheDir: AbsolutePath?

    private let useInMemoryCache: Bool
    private let memoryCacheActor = ManifestCacheActor()

    public init(
        toolchain: UserToolchain,
        serializedDiagnostics: Bool = false,
        isManifestSandboxEnabled: Bool = true,
        useInMemoryCache: Bool = true,
        cacheDir: AbsolutePath? = .none,
        extraManifestFlags: [String]? = .none,
        importRestrictions: (startingToolsVersion: ToolsVersion, allowedImports: [String])? = .none,
        delegate: Delegate? = .none,
        pruneDependencies: Bool = false
    ) {
        self.toolchain = toolchain
        self.serializedDiagnostics = serializedDiagnostics
        self.isManifestSandboxEnabled = isManifestSandboxEnabled
        self.extraManifestFlags = extraManifestFlags ?? []
        self.importRestrictions = importRestrictions

        self.delegate = delegate

        self.useInMemoryCache = useInMemoryCache
        self.databaseCacheDir = try? cacheDir.map(resolveSymlinks)
        self.pruneDependencies = pruneDependencies
    }

    public func load(
        manifestPath: AbsolutePath,
        manifestToolsVersion: ToolsVersion,
        packageIdentity: PackageIdentity,
        packageKind: PackageReference.Kind,
        packageLocation: String,
        packageVersion: (version: Version?, revision: String?)?,
        identityResolver: IdentityResolver,
        dependencyMapper: DependencyMapper,
        fileSystem: FileSystem,
        observabilityScope: ObservabilityScope,
        delegateQueue: DispatchQueue
    ) async throws -> Manifest {
        // Inform the delegate.
        let start = DispatchTime.now()
        delegateQueue.async { [delegate = self.delegate] in
            delegate?.willLoad(
                packageIdentity: packageIdentity,
                packageLocation: packageLocation,
                manifestPath: manifestPath
            )
        }

        // Validate that the file exists.
        guard fileSystem.isFile(manifestPath) else {
            throw PackageModel.Package.Error.noManifest(at: manifestPath, version: packageVersion?.version)
        }

        let parsedManifest = try await self.loadAndCacheManifest(
            at: manifestPath,
            toolsVersion: manifestToolsVersion,
            packageIdentity: packageIdentity,
            packageKind: packageKind,
            packageLocation: packageLocation,
            packageVersion: packageVersion?.version,
            identityResolver: identityResolver,
            dependencyMapper: dependencyMapper,
            fileSystem: fileSystem,
            observabilityScope: observabilityScope,
            delegate: delegate,
            delegateQueue: delegateQueue
        )
        // Convert legacy system packages to the current target‐based model.
        var products = parsedManifest.products
        var targets = parsedManifest.targets
        if products.isEmpty, targets.isEmpty,
            fileSystem.isFile(manifestPath.parentDirectory.appending(component: moduleMapFilename)) {
            try products.append(ProductDescription(
                name: parsedManifest.name,
                type: .library(.automatic),
                targets: [parsedManifest.name])
            )
            targets.append(try TargetDescription(
                name: parsedManifest.name,
                path: "",
                type: .system,
                packageAccess: false,
                pkgConfig: parsedManifest.pkgConfig,
                providers: parsedManifest.providers
            ))
        }

        let manifest = Manifest(
            displayName: parsedManifest.name,
            packageIdentity: packageIdentity,
            path: manifestPath,
            packageKind: packageKind,
            packageLocation: packageLocation,
            defaultLocalization: parsedManifest.defaultLocalization,
            platforms: parsedManifest.platforms,
            version: packageVersion?.version,
            revision: packageVersion?.revision,
            toolsVersion: manifestToolsVersion,
            pkgConfig: parsedManifest.pkgConfig,
            providers: parsedManifest.providers,
            cLanguageStandard: parsedManifest.cLanguageStandard,
            cxxLanguageStandard: parsedManifest.cxxLanguageStandard,
            swiftLanguageVersions: parsedManifest.swiftLanguageVersions,
            dependencies: parsedManifest.dependencies,
            products: products,
            targets: targets,
            traits: parsedManifest.traits,
            pruneDependencies: self.pruneDependencies
        )

        // Inform the delegate.
        delegateQueue.async { [delegate = self.delegate] in
            delegate?.didLoad(
                packageIdentity: packageIdentity,
                packageLocation: packageLocation,
                manifestPath: manifestPath,
                duration: start.distance(to: .now())
            )
        }

        return manifest
    }

    /// Load the JSON string for the given manifest.
    private func parseManifest(
        _ result: EvaluationResult,
        packageIdentity: PackageIdentity,
        packageKind: PackageReference.Kind,
        packagePath: AbsolutePath,
        packageLocation: String,
        toolsVersion: ToolsVersion,
        identityResolver: IdentityResolver,
        dependencyMapper: DependencyMapper,
        fileSystem: FileSystem,
        emitCompilerOutput: Bool,
        observabilityScope: ObservabilityScope,
        delegate: Delegate?,
        delegateQueue: DispatchQueue?
    ) throws -> ManifestJSONParser.Result {
        // Throw now if we weren't able to parse the manifest.
        guard let manifestJSON = result.manifestJSON, !manifestJSON.isEmpty else {
            let errors = result.errorOutput ?? result.compilerOutput ?? "Missing or empty JSON output from manifest compilation for \(packageIdentity)"
            throw ManifestParseError.invalidManifestFormat(errors, diagnosticFile: result.diagnosticFile, compilerCommandLine: result.compilerCommandLine)
        }

        // We should not have any fatal error at this point.
        guard result.errorOutput == nil else {
            throw InternalError("unexpected error output: \(result.errorOutput!)")
        }

        // We might have some non-fatal output (warnings/notes) from the compiler even when
        // we were able to parse the manifest successfully.
        if emitCompilerOutput, let compilerOutput = result.compilerOutput {
            let metadata = result.diagnosticFile.map { diagnosticFile -> ObservabilityMetadata in
                var metadata = ObservabilityMetadata()
                metadata.manifestLoadingDiagnosticFile = diagnosticFile
                return metadata
            }
            observabilityScope.emit(warning: compilerOutput, metadata: metadata)
        }

        let start = DispatchTime.now()
        delegateQueue?.async {
            delegate?.willParse(
                packageIdentity: packageIdentity,
                packageLocation: packageLocation
            )
        }

        let result = try ManifestJSONParser.parse(
            v4: manifestJSON,
            toolsVersion: toolsVersion,
            packageKind: packageKind,
            packagePath: packagePath,
            identityResolver: identityResolver,
            dependencyMapper: dependencyMapper,
            fileSystem: fileSystem
        )
        delegateQueue?.async {
            delegate?.didParse(
                packageIdentity: packageIdentity,
                packageLocation: packageLocation,
                duration: start.distance(to: .now())
            )
        }
        return result
    }

    private func loadAndCacheManifest(
        at path: AbsolutePath,
        toolsVersion: ToolsVersion,
        packageIdentity: PackageIdentity,
        packageKind: PackageReference.Kind,
        packageLocation: String,
        packageVersion: Version?,
        identityResolver: IdentityResolver,
        dependencyMapper: DependencyMapper,
        fileSystem: FileSystem,
        observabilityScope: ObservabilityScope,
        delegate: Delegate?,
        delegateQueue: DispatchQueue?
    ) async throws -> ManifestJSONParser.Result {
        let key = try CacheKey(
            packageIdentity: packageIdentity,
            packageLocation: packageLocation,
            manifestPath: path,
            toolsVersion: toolsVersion,
            env: Environment.current.cachable,
            swiftpmVersion: SwiftVersion.current.displayString,
            extraManifestFlags: self.extraManifestFlags,
            fileSystem: fileSystem
        )

        // try from in-memory cache
        if self.useInMemoryCache, let parsedManifest = await self.memoryCacheActor.get(key: key) {
            observabilityScope.emit(debug: "loading manifest for '\(packageIdentity)' v. \(packageVersion?.description ?? "unknown") from memory cache")
            return parsedManifest
        }

        // initialize db cache
        let dbCache = self.databaseCacheDir.map { cacheDir in
            let path = Self.manifestCacheDBPath(cacheDir)
            var configuration = SQLiteBackedCacheConfiguration()
            // FIXME: expose as user-facing configuration
            configuration.maxSizeInMegabytes = 100
            configuration.truncateWhenFull = true
            return SQLiteBackedCache<EvaluationResult>(
                tableName: "MANIFEST_CACHE",
                location: .path(path),
                configuration: configuration
            )
        }
        // Ensure dbCache is closed on exit
        defer {
            do {
                try dbCache?.close()
            } catch {
                observabilityScope.emit(
                    warning: "failed closing manifest db cache",
                    underlyingError: error
                )
            }
        }


        do {
            // try to get it from the cache
            if let evaluationResult = try dbCache?.get(key: key.sha256Checksum), let manifestJSON = evaluationResult.manifestJSON, !manifestJSON.isEmpty {
                observabilityScope.emit(debug: "loading manifest for '\(packageIdentity)' v. \(packageVersion?.description ?? "unknown") from db cache")
                let parsedManifest = try self.parseManifest(
                    evaluationResult,
                    packageIdentity: packageIdentity,
                    packageKind: packageKind,
                    packagePath: path.parentDirectory,
                    packageLocation: packageLocation,
                    toolsVersion: toolsVersion,
                    identityResolver: identityResolver,
                    dependencyMapper: dependencyMapper,
                    fileSystem: fileSystem,
                    emitCompilerOutput: false,
                    observabilityScope: observabilityScope,
                    delegate: delegate,
                    delegateQueue: delegateQueue // Pass the delegate queue
                )
                // Store in memory cache if enabled
                if self.useInMemoryCache {
                    await self.memoryCacheActor.set(key: key, value: parsedManifest)
                }
                return parsedManifest
            }
        } catch {
            observabilityScope.emit(
                warning: "failed loading cached manifest for '\(key.packageIdentity)'",
                underlyingError: error
            )
        }

        // shells out and compiles the manifest, finally output a JSON
        observabilityScope.emit(debug: "evaluating manifest for '\(packageIdentity)' v. \(packageVersion?.description ?? "unknown")")

        let evaluationResult = try await self.evaluateManifest(
            packageIdentity: key.packageIdentity,
            packageLocation: packageLocation,
            manifestPath: key.manifestPath,
            manifestContents: key.manifestContents,
            toolsVersion: key.toolsVersion,
            observabilityScope: observabilityScope,
            delegate: delegate,
            delegateQueue: delegateQueue // Pass the delegate queue
        )

        // only cache successfully parsed manifests
        let parsedManifest = try self.parseManifest(
            evaluationResult,
            packageIdentity: packageIdentity,
            packageKind: packageKind,
            packagePath: path.parentDirectory,
            packageLocation: packageLocation,
            toolsVersion: toolsVersion,
            identityResolver: identityResolver,
            dependencyMapper: dependencyMapper,
            fileSystem: fileSystem,
            emitCompilerOutput: true,
            observabilityScope: observabilityScope,
            delegate: delegate,
            delegateQueue: delegateQueue
        )

        do {
            // Store in memory cache
            await self.memoryCacheActor.set(key: key, value: parsedManifest)
            // Store in db cache
            try dbCache?.put(key: key.sha256Checksum, value: evaluationResult, observabilityScope: observabilityScope)
        } catch {
            observabilityScope.emit(
                warning: "failed storing manifest for '\(key.packageIdentity)' in cache",
                underlyingError: error
            )
        }

        return parsedManifest
    }

    private func validateImports(
        manifestPath: AbsolutePath,
        toolsVersion: ToolsVersion
    ) async throws {
        // If there are no import restrictions, we do not need to validate.
        guard let importRestrictions = self.importRestrictions, toolsVersion >= importRestrictions.startingToolsVersion else {
            return
        }

        // Allowed are the expected defaults, plus anything allowed by the configured restrictions.
        let allowedImports = [
            "PackageDescription",
            "Swift",
            "SwiftOnoneSupport",
            "_SwiftConcurrencyShims"
        ] + importRestrictions.allowedImports

        let importScanner = SwiftcImportScanner(
            swiftCompilerEnvironment: self.toolchain.swiftCompilerEnvironment,
            swiftCompilerFlags: self.extraManifestFlags,
            swiftCompilerPath: self.toolchain.swiftCompilerPathForManifests
        )
        let result = try await importScanner.scanImports(manifestPath)

        let imports = result.filter { !allowedImports.contains($0) }
        guard imports.isEmpty else {
            throw ManifestParseError.importsRestrictedModules(imports)
        }
    }

    /// Compiler the manifest at the given path and retrieve the JSON.
    fileprivate func evaluateManifest(
        packageIdentity: PackageIdentity,
        packageLocation: String,
        manifestPath: AbsolutePath,
        manifestContents: [UInt8],
        toolsVersion: ToolsVersion,
        observabilityScope: ObservabilityScope,
        delegate: Delegate?,
        delegateQueue: DispatchQueue?
    ) async throws -> EvaluationResult {
        let manifestPreamble: ByteString
        if toolsVersion >= .v5_8 {
            manifestPreamble = ByteString()
        } else {
            manifestPreamble = ByteString("\nimport Foundation")
        }

        return try await Basics.withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in
            let manifestTempFilePath = tempDir.appending("manifest.swift")
            // Since this isn't overwriting the original file, append Foundation library
            // import to avoid having diagnostics being displayed on the incorrect line.
            try localFileSystem.writeFileContents(manifestTempFilePath, bytes: ByteString(manifestContents + manifestPreamble.contents))

            let vfsOverlayTempFilePath = tempDir.appending("vfs.yaml")
            try VFSOverlay(roots: [
                VFSOverlay.File(
                    name: manifestPath._normalized.replacing(#"\"#, with: #"\\"#),
                    externalContents: manifestTempFilePath._nativePathString(escaped: true)
                )
            ]).write(to: vfsOverlayTempFilePath, fileSystem: localFileSystem)

            try await validateImports(
                manifestPath: manifestTempFilePath,
                toolsVersion: toolsVersion
            )
            let result = try await self.evaluateManifest(
                at: manifestPath,
                vfsOverlayPath: vfsOverlayTempFilePath,
                packageIdentity: packageIdentity,
                packageLocation: packageLocation,
                toolsVersion: toolsVersion,
                observabilityScope: observabilityScope,
                delegate: delegate,
                delegateQueue: delegateQueue
            )

            return result
        }
    }

    /// Helper method for evaluating the manifest.
    func evaluateManifest(
        at manifestPath: AbsolutePath,
        vfsOverlayPath: AbsolutePath? = nil,
        packageIdentity: PackageIdentity,
        packageLocation: String,
        toolsVersion: ToolsVersion,
        observabilityScope: ObservabilityScope,
        delegate: Delegate?,
        delegateQueue: DispatchQueue?
    ) async throws -> EvaluationResult {
        // The compiler has special meaning for files with extensions like .ll, .bc etc.
        // Assert that we only try to load files with extension .swift to avoid unexpected loading behavior.
        guard manifestPath.extension == "swift" else {
            throw InternalError("Manifest files must contain .swift suffix in their name, given: \(manifestPath).")
        }

        var evaluationResult = EvaluationResult()

        // For now, we load the manifest by having Swift interpret it directly.
        // Eventually, we should have two loading processes, one that loads only
        // the declarative package specification using the Swift compiler directly
        // and validates it.

        // Compute the path to runtime we need to load.
        let runtimePath = self.toolchain.swiftPMLibrariesLocation.manifestLibraryPath

        // FIXME: Workaround for the module cache bug that's been haunting Swift CI
        // <rdar://problem/48443680>
        let moduleCachePath = try (
            Environment.current["SWIFTPM_MODULECACHE_OVERRIDE"] ??
            Environment.current["SWIFTPM_TESTS_MODULECACHE"]).flatMap { try AbsolutePath(validating: $0) }

        var cmd: [String] = []
        cmd += [self.toolchain.swiftCompilerPathForManifests.pathString]

        if let vfsOverlayPath {
            cmd += ["-vfsoverlay", vfsOverlayPath.pathString]
        }

        // if runtimePath is set to "PackageFrameworks" that means we could be developing SwiftPM in Xcode
        // which produces a framework for dynamic package products.
        if runtimePath.extension == "framework" {
            cmd += [
                "-F", runtimePath.parentDirectory.pathString,
                "-Xlinker", "-rpath", "-Xlinker", runtimePath.parentDirectory.pathString,
            ]

            // Explicitly link `AppleProductTypes` since auto-linking won't work here.
#if ENABLE_APPLE_PRODUCT_TYPES
            cmd += ["-framework", "AppleProductTypes"]
#else
            cmd += ["-framework", "PackageDescription"]
#endif
        } else {
            cmd += [
                "-L", runtimePath.pathString,
                "-lPackageDescription",
            ]
#if !os(Windows)
            // -rpath argument is not supported on Windows,
            // so we add runtimePath to PATH when executing the manifest instead
            cmd += ["-Xlinker", "-rpath", "-Xlinker", runtimePath.pathString]
#endif
        }

        // Use the same minimum deployment target as the PackageDescription library (with a fallback to the default host triple).
#if os(macOS)
        if let version = self.toolchain.swiftPMLibrariesLocation.manifestLibraryMinimumDeploymentTarget?.versionString {
            cmd += ["-target", "\(self.toolchain.targetTriple.tripleString(forPlatformVersion: version))"]
        } else {
            cmd += ["-target", self.toolchain.targetTriple.tripleString]
        }
#endif

        // Add any extra flags required as indicated by the ManifestLoader.
        cmd += self.toolchain.swiftCompilerFlags

        cmd += self.interpreterFlags(for: toolsVersion)
        if let moduleCachePath {
            cmd += ["-module-cache-path", moduleCachePath.pathString]
        }

        // Add the arguments for emitting serialized diagnostics, if requested.
        if self.serializedDiagnostics, let databaseCacheDir = self.databaseCacheDir {
            let diaDir = databaseCacheDir.appending("ManifestLoading")
            let diagnosticFile = diaDir.appending("\(packageIdentity).dia")
            try localFileSystem.createDirectory(diaDir, recursive: true)
            cmd += ["-Xfrontend", "-serialize-diagnostics-path", "-Xfrontend", diagnosticFile.pathString]
            evaluationResult.diagnosticFile = diagnosticFile
        }

        cmd += [manifestPath._normalized]

        cmd += self.extraManifestFlags

        // Compile the manifest in a temporary directory
        return try await Basics.withTemporaryDirectory(removeTreeOnDeinit: true) { tmpDir in
            // Set path to compiled manifest executable.
            #if os(Windows)
            let executableSuffix = ".exe"
            #else
            let executableSuffix = ""
            #endif
            let compiledManifestFile = tmpDir.appending("\(packageIdentity)-manifest\(executableSuffix)")
            cmd += ["-o", compiledManifestFile.pathString]

            evaluationResult.compilerCommandLine = cmd

            delegateQueue?.async { [delegate = self.delegate] in
                delegate?.willCompile(
                    packageIdentity: packageIdentity,
                    packageLocation: packageLocation,
                    manifestPath: manifestPath
                )
            }
            // Compile the manifest.
            let compileStart = DispatchTime.now()
            let compilerResult: AsyncProcessResult
            do {
                 compilerResult = try await AsyncProcess.popen(arguments: cmd, environment: self.toolchain.swiftCompilerEnvironment)
                 evaluationResult.compilerOutput = try (compilerResult.utf8Output() + compilerResult.utf8stderrOutput()).spm_chuzzle()
            } catch {
                delegateQueue?.async { [delegate = self.delegate] in
                    delegate?.didCompile(
                        packageIdentity: packageIdentity,
                        packageLocation: packageLocation,
                        manifestPath: manifestPath,
                        duration: compileStart.distance(to: .now())
                    )
                }
                throw error // Re-throw process errors
            }

            delegateQueue?.async { [delegate = self.delegate] in
                delegate?.didCompile(
                    packageIdentity: packageIdentity,
                    packageLocation: packageLocation,
                    manifestPath: manifestPath,
                    duration: compileStart.distance(to: .now())
                )
            }

            // Return now if there was a compilation error.
            if compilerResult.exitStatus != .terminated(code: 0) {
                // If there's compiler output, it's a format error. Otherwise, maybe something else went wrong.
                 evaluationResult.errorOutput = evaluationResult.compilerOutput ?? "Manifest compilation failed with exit status \(compilerResult.exitStatus)"
                 return evaluationResult // Return the result containing the error output
            }

            // Pass an open file descriptor of a file to which the JSON representation of the manifest will be written.
            let jsonOutputFile = tmpDir.appending("\(packageIdentity)-output.json")
            guard let jsonOutputFileDesc = fopen(jsonOutputFile.pathString, "w") else {
                 throw StringError("couldn't create the manifest's JSON output file")
            }
            // Ensure the file is closed
            defer { fclose(jsonOutputFileDesc) }


            var runCmd = [compiledManifestFile.pathString]
            #if os(Windows)
            // NOTE: `_get_osfhandle` returns a non-owning, unsafe,
            // unretained HANDLE.  DO NOT invoke `CloseHandle` on `hFile`.
            let hFile: Int = _get_osfhandle(_fileno(jsonOutputFileDesc))
            runCmd += ["-handle", "\(String(hFile, radix: 16))"]
            #else
            runCmd += ["-fileno", "\(fileno(jsonOutputFileDesc))"]
            #endif

            do {
                let packageDirectory = manifestPath.parentDirectory.pathString

                let gitInformation: ContextModel.GitInformation?
                do {
                    let repo = GitRepository(path: manifestPath.parentDirectory)
                    // These Git operations might block, consider making them async if performance is critical
                    gitInformation = ContextModel.GitInformation(
                        currentTag: repo.getCurrentTag(),
                        currentCommit: try repo.getCurrentRevision().identifier,
                        hasUncommittedChanges: repo.hasUncommittedChanges()
                    )
                } catch {
                    // Ignore errors getting git info
                    gitInformation = nil
                }

                let contextModel = ContextModel(
                    packageDirectory: packageDirectory,
                    gitInformation: gitInformation
                )
                runCmd += ["-context", try contextModel.encode()]
            } catch {
                throw error // Re-throw encoding errors
            }

            // If enabled, run command in a sandbox.
            // This provides some safety against arbitrary code execution when parsing manifest files.
            // We only allow the permissions which are absolutely necessary.
            if self.isManifestSandboxEnabled {
                let cacheDirectories = [self.databaseCacheDir?.appending("ManifestLoading"), moduleCachePath].compactMap{ $0 }
                let strictness: Sandbox.Strictness = toolsVersion < .v5_3 ? .manifest_pre_53 : .default
                do {
                    runCmd = try Sandbox.apply(command: runCmd, fileSystem: localFileSystem, strictness: strictness, writableDirectories: cacheDirectories)
                } catch {
                    throw error // Re-throw sandbox errors
                }
            }

            // Run the compiled manifest.
            let evaluationStart = DispatchTime.now()

            delegateQueue?.async { [delegate = self.delegate] in
                delegate?.willEvaluate(
                    packageIdentity: packageIdentity,
                    packageLocation: packageLocation,
                    manifestPath: manifestPath
                )
            }

            var environment = Environment.current
            #if os(Windows)
            let windowsPathComponent = runtimePath.pathString.replacing("/", with: "\\")
            environment.prependPath(key: .path, value: windowsPathComponent)
            #endif

            let runResult: AsyncProcessResult
            do {
                runResult = try await AsyncProcess.popen(arguments: runCmd, environment: environment)
                if let runOutput = try (runResult.utf8Output() + runResult.utf8stderrOutput()).spm_chuzzle() {
                    // Append the runtime output to any compiler output we've received.
                    evaluationResult.compilerOutput = (evaluationResult.compilerOutput ?? "") + runOutput
                }
            } catch {
                delegateQueue?.async { [delegate = self.delegate] in
                    delegate?.didEvaluate(
                        packageIdentity: packageIdentity,
                        packageLocation: packageLocation,
                        manifestPath: manifestPath,
                        duration: evaluationStart.distance(to: .now())
                    )
                }
                throw error // Re-throw process errors
            }

            delegateQueue?.async { [delegate = self.delegate] in
                delegate?.didEvaluate(
                    packageIdentity: packageIdentity,
                    packageLocation: packageLocation,
                    manifestPath: manifestPath,
                    duration: evaluationStart.distance(to: .now())
                )
            }

            // Return now if there was a runtime error.
            if runResult.exitStatus != .terminated(code: 0) {
                 // The runtime output is the error
                 evaluationResult.errorOutput = evaluationResult.compilerOutput
                 return evaluationResult // Return the result containing the error output
            }

            // Read the JSON output that was emitted by libPackageDescription.
            let jsonOutput: String = try localFileSystem.readFileContents(jsonOutputFile)
            evaluationResult.manifestJSON = jsonOutput

            // withTemporaryDirectory handles cleanup automatically
            return evaluationResult
        }
    }

    package static func interpreterFlags(
        for toolsVersion: ToolsVersion,
        toolchain: some Toolchain
    ) -> [String] {
        var cmd = [String]()
        let modulesPath = toolchain.swiftPMLibrariesLocation.manifestModulesPath
        cmd += ["-swift-version", toolsVersion.swiftLanguageVersion.rawValue]
        // if runtimePath is set to "PackageFrameworks" that means we could be developing SwiftPM in Xcode
        // which produces a framework for dynamic package products.
        if modulesPath.extension == "framework" {
            cmd += ["-I", modulesPath.parentDirectory.parentDirectory.pathString]
        } else {
            cmd += ["-I", modulesPath.pathString]
        }
      #if os(macOS)
        if let sdkRoot = toolchain.sdkRootPath {
            cmd += ["-sdk", sdkRoot.pathString]
        }
      #endif
        cmd += ["-package-description-version", toolsVersion.description]
        return cmd
    }

    /// Returns the interpreter flags for a manifest.
    public func interpreterFlags(
        for toolsVersion: ToolsVersion
    ) -> [String] {
        return Self.interpreterFlags(for: toolsVersion, toolchain: toolchain)
    }

    /// Returns path to the manifest database inside the given cache directory.
    private static func manifestCacheDBPath(_ cacheDir: AbsolutePath) -> AbsolutePath {
        return cacheDir.appending("manifest.db")
    }

    /// reset internal cache
    public func resetCache(observabilityScope: ObservabilityScope) async {
        await self.memoryCacheActor.clear()
    }

    /// reset internal state and purge shared cache
    public func purgeCache(observabilityScope: ObservabilityScope) async {
        await self.resetCache(observabilityScope: observabilityScope)

        guard let manifestCacheDBPath = self.databaseCacheDir.flatMap({ Self.manifestCacheDBPath($0) }) else {
            return
        }

        guard localFileSystem.exists(manifestCacheDBPath) else {
            return
        }

        observabilityScope.emit(info: "Purging manifest cache at '\(manifestCacheDBPath)'")

        do {
            try localFileSystem.removeFileTree(manifestCacheDBPath)
        } catch {
            observabilityScope.emit(
                error: "Error purging manifests cache at '\(manifestCacheDBPath)'",
                underlyingError: error
            )
        }
    }
}

extension ManifestLoader {
    struct CacheKey: Hashable {
        let packageIdentity: PackageIdentity
        let manifestPath: AbsolutePath
        let manifestContents: [UInt8]
        let toolsVersion: ToolsVersion
        let env: Environment
        let swiftpmVersion: String
        let sha256Checksum: String

        init (packageIdentity: PackageIdentity,
              packageLocation: String,
              manifestPath: AbsolutePath,
              toolsVersion: ToolsVersion,
              env: Environment,
              swiftpmVersion: String,
              extraManifestFlags: [String],
              fileSystem: FileSystem
        ) throws {
            let manifestContents = try fileSystem.readFileContents(manifestPath).contents
            let sha256Checksum = try Self.computeSHA256Checksum(
                packageIdentity: packageIdentity,
                packageLocation: packageLocation,
                manifestContents: manifestContents,
                toolsVersion: toolsVersion,
                env: env,
                extraManifestFlags: extraManifestFlags,
                swiftpmVersion: swiftpmVersion
            )

            self.packageIdentity = packageIdentity
            self.manifestPath = manifestPath
            self.manifestContents = manifestContents
            self.toolsVersion = toolsVersion
            self.env = env
            self.swiftpmVersion = swiftpmVersion
            self.sha256Checksum = sha256Checksum
        }

        func hash(into hasher: inout Hasher) {
            hasher.combine(self.sha256Checksum)
        }

        private static func computeSHA256Checksum(
            packageIdentity: PackageIdentity,
            packageLocation: String,
            manifestContents: [UInt8],
            toolsVersion: ToolsVersion,
            env: Environment,
            extraManifestFlags: [String],
            swiftpmVersion: String
        ) throws -> String {
            let stream = BufferedOutputByteStream()
            stream.send(packageIdentity)
            stream.send(packageLocation)
            stream.send(manifestContents)
            stream.send(toolsVersion.description)
            for (key, value) in env.sorted(by: { $0.key > $1.key }) {
                stream.send(key.rawValue).send(value)
            }
            stream.send(swiftpmVersion)
            for flag in extraManifestFlags {
                stream.send(flag)
            }
            return stream.bytes.sha256Checksum
        }
    }
}

extension ManifestLoader {
    struct EvaluationResult: Codable {
        /// The path to the diagnostics file (.dia).
        ///
        /// This is only present if serialized diagnostics are enabled.
        var diagnosticFile: AbsolutePath?

        /// The output from compiler, if any.
        ///
        /// This would contain the errors and warnings produced when loading the manifest file.
        var compilerOutput: String?

        /// The manifest in JSON format.
        var manifestJSON: String?

        /// The command line used to compile the manifest
        var compilerCommandLine: [String]?

        /// Any non-compiler error that might have occurred during manifest loading.
        ///
        /// For e.g., we could have failed to spawn the process or create temporary file.
        var errorOutput: String? {
            didSet {
                assert(self.manifestJSON == nil)
            }
        }

        var hasErrors: Bool {
            return self.manifestJSON == nil
        }
    }
}

extension ManifestLoader {
    /// Represents behavior that can be deferred until a more appropriate time.
    struct DelayableAction<T> {
        var target: T?
        var action: ((T) -> Void)?

        func perform() {
            if let value = target, let cleanup = action {
                cleanup(value)
            }
        }

        mutating func delay() -> DelayableAction {
            let next = DelayableAction(target: target, action: action)
            target = nil
            action = nil
            return next
        }
    }
}
